package net.sourceforge.openforecast.models;

import java.util.Iterator;

import net.sourceforge.openforecast.AccuracyIndicators;
import net.sourceforge.openforecast.DataPoint;
import net.sourceforge.openforecast.DataSet;
import net.sourceforge.openforecast.ForecastingModel;

/** This class implements a variety of methods that are common across all forecasting models. In particular, the
 * calculation of the accuracy indicators can be generalized, and is therefore implemented in the method,
 * calculateAccuracyIndicators.
 * 
 * @see #calculateAccuracyIndicators */
public abstract class AbstractForecastingModel extends AccuracyIndicators implements ForecastingModel {
	/** Remembers whether this model has been properly initialized. */
	protected boolean initialized = false;

	/** Default constructor. */
	protected AbstractForecastingModel() {
		super();
	}

	@Override
	public boolean isInitialized() {
		return initialized;
	}

	protected void checkInitialized() {
		if (!isInitialized())
			throw new ModelNotInitializedException();
	}

	/** Using the current model parameters (initialized in init), apply the forecast model to the given data set. Each
	 * data point in the data set must have valid values for the independent variables. Upon return, the value of the
	 * dependent variable will be updated with the forecast values computed. This method is provided as a convenience
	 * method, and iterates through the data set invoking forecast(DataPoint) to do the actual forecast for each data
	 * point. In general, it is not necessary to override this method. However, if a subclass can provide a more
	 * efficient approach then it is recommended that the subclass provide its own implementation.
	 * 
	 * @param dataSet the set of data points for which forecast values (for the dependent variable) are required.
	 * @return the same data set passed in but with the dependent values updated to contain the new forecast values.
	 * @throws ModelNotInitializedException if getMSE is called before the model has been initialized with a call to
	 *         init. */
	public DataSet forecast(DataSet dataSet) {
		checkInitialized();

		dataSet.forEach(x -> x.setValue(forecast(x)));

		return dataSet;
	}

	/** A helper method to calculate the various accuracy indicators when applying the given DataSet to the current
	 * forecasting model.
	 * 
	 * @param dataSet the DataSet to use to evaluate this model, and to calculate the accuracy indicators against. */
	protected void calculateAccuracyIndicators(DataSet dataSet) {
		// Note that the model has been initialized
		initialized = true;

		// Reset various helper summations
		double sumErr = 0.0;
		double sumAbsErr = 0.0;
		double sumAbsPercentErr = 0.0;
		double sumErrSquared = 0.0;

		// Obtain the forecast values for this model
		DataSet forecastValues = new DataSet(dataSet);
		forecast(forecastValues);

		// Calculate the Sum of the Absolute Errors
		Iterator<DataPoint> itForecast = forecastValues.iterator();
		for (DataPoint dp : dataSet) {
			// Get next data point
			double x = dp.getValue();

			// Get next forecast value
			DataPoint dpForecast = itForecast.next();
			double forecastValue = dpForecast.getValue();

			// Calculate error in forecast, and update sums appropriately
			double error = forecastValue - x;
			sumErr += error;
			sumAbsErr += Math.abs(error);
			sumAbsPercentErr += Math.abs(error / x);
			sumErrSquared += error * error;
		}

		// Initialize the accuracy indicators
		int n = dataSet.size();
		int p = getNumberOfPredictors();

		setAIC(n * Math.log(2 * Math.PI) + Math.log(sumErrSquared / n) + 2 * (p + 2));
		setBias(sumErr / n);
		setMAD(sumAbsErr / n);
		setMAPE(sumAbsPercentErr / n);
		setMSE(sumErrSquared / n);
		setSAE(sumAbsErr);
	}
}